/*
 * Copyright 2010 Marco van Wieren
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 * 
 * @author 	Marco van Wieren
 */
package com.mvw.html5.client;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.ToggleButton;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.widgetideas.graphics.client.Color;
import com.google.gwt.widgetideas.graphics.client.GWTCanvas;

/**
 * Entry point classes define <code>onModuleLoad()</code>.
 */
public class GWTSnake implements EntryPoint {
	
	private GWTCanvas canvas = null;
	private SnakePanel focusPanel = null;
	
	private Map<String, Double> currentPosition = new HashMap<String, Double>();
	private final double gridDimension = 10;
	
	private Direction direction = Direction.RIGHT;
	
	private LinkedList<Square> snake = new LinkedList<Square>();
	private int snakeBodyLength = 2;
	private Square foodItem = null;
	
	private Timer timer = null;
	
	private TextBox tbScore = null;
	private Label lblGameOver = null;;
	private int score = 0;
	
	public void onModuleLoad() {
		VerticalPanel main = new VerticalPanel();
		main.setPixelSize(400, 450);
		
		// scorePanel
		HorizontalPanel scorePanel = new HorizontalPanel();
		scorePanel.setPixelSize(400, 50);
		
		Label lblSnake = new Label("SNAKE");
		lblSnake.setStyleName("lblSnake");
		scorePanel.add(lblSnake);
		scorePanel.setCellHorizontalAlignment(lblSnake, HorizontalPanel.ALIGN_LEFT);
		
		Label lblScore = new Label("Score: ");
		lblScore.setStyleName("lblScore");
		scorePanel.add(lblScore);
		scorePanel.setCellHorizontalAlignment(lblScore, HorizontalPanel.ALIGN_RIGHT);
		
		tbScore = new TextBox();
		tbScore.setText(String.valueOf(score));
		tbScore.setStyleName("tbScore");
		scorePanel.add(tbScore);
		scorePanel.setCellHorizontalAlignment(tbScore, HorizontalPanel.ALIGN_RIGHT);
		
		// controlPanel
		HorizontalPanel controlPanel = new HorizontalPanel();
		controlPanel.setPixelSize(400, 50);
		
		final ToggleButton btnPause = new ToggleButton("Pause", "Resume");
		btnPause.addClickHandler(new ClickHandler() {
			
			@Override
			public void onClick(ClickEvent event) {
				if(btnPause.isDown()) {
					timer.cancel();
				} else {
					timer.scheduleRepeating(100);
				}
				focusPanel.setFocus(true);
			}
		});
		btnPause.setStyleName("appleButton");
		controlPanel.add(btnPause);
		controlPanel.setCellHorizontalAlignment(btnPause, HorizontalPanel.ALIGN_LEFT);
		
		final Button btnRestart = new Button("Restart");
		btnRestart.addClickHandler(new ClickHandler() {
			
			@Override
			public void onClick(ClickEvent event) {
				initSnake();
			}
		});
		btnRestart.setStyleName("appleButton");
		controlPanel.add(btnRestart);
		controlPanel.setCellHorizontalAlignment(btnRestart, HorizontalPanel.ALIGN_LEFT);
		
		lblGameOver = new Label("");
		lblGameOver.setStyleName("lblGameOver");
		controlPanel.add(lblGameOver);
		controlPanel.setCellHorizontalAlignment(lblGameOver, HorizontalPanel.ALIGN_RIGHT);
		
		// Instruction panel
		HorizontalPanel instructionPanel = new HorizontalPanel();
		instructionPanel.setPixelSize(400, 100);
		instructionPanel.setStyleName("lblInstructions");
		
		HTML lblInstructions = new HTML("Control the snake by using the arrow keys<br/>You'll find this project's source code <a target='_blank' href='http://code.google.com/p/gwtsnake/'>here</a>");
		
		instructionPanel.add(lblInstructions);
		instructionPanel.setCellHorizontalAlignment(lblInstructions, HorizontalPanel.ALIGN_CENTER);
		
		// Use a (slightly modified) focus panel to be able to attach a KeyDownHandler
		focusPanel = new SnakePanel();
		focusPanel.setPixelSize(402, 302);
		canvas = new GWTCanvas(400, 300);
		focusPanel.add(canvas);
		
		main.add(scorePanel);
		main.add(controlPanel);
		main.add(focusPanel);
		main.add(instructionPanel);
		RootPanel.get("snake-canvas").add(main);
		
		// Set style
		canvas.setStylePrimaryName("canvas");
		focusPanel.setStylePrimaryName("focusPanel");
		
		// Add MouseDownHandler
		HandlerRegistration keyDownHandler = focusPanel.addKeyDownHandler(new KeyDownHandler() {

			@Override
			public void onKeyDown(KeyDownEvent event) {
				if(event.isLeftArrow()) {
					if(direction != Direction.RIGHT) {
						direction = Direction.LEFT;
					}
				}
				if(event.isUpArrow()) {
					if(direction != Direction.DOWN) {
						direction = Direction.UP;
					}
				}
				if(event.isRightArrow()) {
					if(direction != Direction.LEFT) {
						direction = Direction.RIGHT;
					}
				}
				if(event.isDownArrow()) {
					if(direction != Direction.UP) {
						direction = Direction.DOWN;
					}
				}
				if(event.isControlKeyDown()) {
					if(timer != null) {
						timer.cancel();
					}
				}
			}
		});
		
		// Start the ganme
		initSnake();
	}
	
	private void initSnake() {
		// Clean up (in case of a restart
		if(timer != null) {
			timer.cancel();
		}
		canvas.clear();
		snake.clear();
		lblGameOver.setText("");
		
		// Set initial parameters
		snakeBodyLength = 2;
		score = 0;
		
		double randX = Math.floor(Math.random() * (canvas.getCoordWidth() / gridDimension)) * gridDimension;
		double randY = Math.floor(Math.random() * (canvas.getCoordHeight() / gridDimension)) * gridDimension;
		currentPosition.clear();
		currentPosition.put("x", randX);
		currentPosition.put("y", randY);
		Square square = new Square(randX, randY);
		snake.add(square);
		
		// Set focus
		focusPanel.setFocus(true);
		
		// Start the snake using a repeating timer
		if(timer == null) {
			timer = new Timer() {

				@Override
				public void run() {
					moveSnake();
					
				}
			};
		}
		timer.scheduleRepeating(100);
		
		makeFoodItem();
	}
	
	private void moveLeft() {
		double result = currentPosition.get("x") - gridDimension;
		// Test for the left canvas boundary
		if(result < 0) {
			// Go down when in the upper left quadrant
			if(currentPosition.get("y") < 0.5 * canvas.getCoordHeight()) {
				direction = Direction.DOWN;
				moveDown();
				return;
			// Go up when in the lower left quadrant
			} else {
				direction = Direction.UP;
				moveUp();
				return;
			}
		}
		// Move left when boundary is not touched
		currentPosition.put("x", result);
	}
	
	private void moveDown() {
		double result = currentPosition.get("y") + gridDimension;
		// Test for the lower canvas boundary
		if(result >= canvas.getCoordHeight()) {
			// Go right when in the lower left quadrant
			if(currentPosition.get("x") < 0.5 * canvas.getCoordWidth()) {
				direction = Direction.RIGHT;
				moveRight();
				return;
			// Go left when in the lower right quandrant
			} else {
				direction = Direction.LEFT;
				moveLeft();
				return;
			}
		}
		// Move down when boundary is not touched
		currentPosition.put("y", result);
	}
	
	private void moveRight() {
		// Test for the right canvas boundary
		double result = currentPosition.get("x") + gridDimension;
		if(result >= canvas.getCoordWidth()) {
			// Go down when in the upper right quadrant
			if(currentPosition.get("y") < 0.5 * canvas.getCoordHeight()) {
				direction = Direction.DOWN;
				moveDown();
				return;
			// Go up when in the lower right quadrant
			} else {
				direction = Direction.UP;
				moveUp();
				return;
			}
		}
		// Move right when boundary is not touched
		currentPosition.put("x", result);
	}
	
	private void moveUp() {
		double result = currentPosition.get("y") - gridDimension;
		// Test for the upper canvas boundary
		if(result < 0) {
			// Go right when in the upper left quadrant
			if(currentPosition.get("x") < 0.5 * canvas.getCoordWidth()) {
				direction = Direction.RIGHT;
				moveRight();
				return;
			// Go left when in the upper right quadrant
			} else {
				direction = Direction.LEFT;
				moveLeft();
				return;
			}
		}
		// Move up when boundary is not touched
		currentPosition.put("y", result);
	}
	
	
	private void moveSnake() {
		switch(direction) {
			case LEFT:
				moveLeft();
				drawSnake();
				break;
			case UP:
				moveUp();
				drawSnake();
				break;
			case RIGHT:
				moveRight();
				drawSnake();
				break;
			case DOWN:
				moveDown();
				drawSnake();
				break;
			default:
				break;
		}
	}
	
	private void drawSnake() {
		Square square = new Square(currentPosition.get("x"), currentPosition.get("y"));
		// Test whether the snake bites its own tail :-)
		if(snake.contains(square)) {
			timer.cancel();
			canvas.clear();
			lblGameOver.setText("GAME OVER");
		}
		snake.add(square);
		// This sets the fill color to red
		canvas.setFillStyle(new Color("#ff0000"));
		canvas.fillRect(square.X, 
				square.Y, 
				gridDimension, 
				gridDimension);
		if(snake.size() > snakeBodyLength) {
			// This sets the fill color to black
			canvas.setFillStyle(new Color("#000"));
			canvas.fillRect(snake.get(0).X, 
					snake.get(0).Y, 
					gridDimension, 
					gridDimension);
			snake.remove(0);
		}
		// If the snake just passed over a foodItem its body shall grow by one square unit
		if (square.equals(foodItem)) {
			snakeBodyLength++;
			makeFoodItem();
			score++;
			tbScore.setText(String.valueOf(score));
		}
	}
	
	/*
	 * Draws random food items for the snake onto the canvas
	 */
	private void makeFoodItem() {
		double randX = Math.floor(Math.random() * (canvas.getCoordWidth() / gridDimension)) * gridDimension;
		double randY = Math.floor(Math.random() * (canvas.getCoordHeight() / gridDimension)) * gridDimension;
		Square square = new Square(randX, randY);
		// Don't create a food item where the snake currently is
		if(snake.contains(square)) {
			makeFoodItem();
		} else {
			foodItem = square;
			// This sets the fill color to lime green
			canvas.setFillStyle(new Color("#00ff00"));
			canvas.fillRect(foodItem.X, 
					foodItem.Y, 
					gridDimension, 
					gridDimension);
		}
	}

}
